import os
import re
from pathlib import PurePath
from typing import List, Tuple, Dict

from lib import file_utils, utils, caches, ssh, logger
from lib.sftp import SftpTransporter

# 字符窜行首斜杠正则
_pattern = re.compile(r'^[\\/]*')


class Upload:
    # 项目根目录
    local_basepath: str = None
    # 项目上传缓存记录
    caches_data: dict = None
    # 上传路径是否包括include.paths中配置的路径
    include_append = True
    # 本地要上传的文件夹
    include_dirs: List[str] = None
    # 排除的目录
    exclude_dirs: List[str] = None
    # 项目根目录下的配置参数
    config = None
    # 版本号
    version: str or None = None
    # 项目根目录.hornet.version.latest文件中的版本号
    latest_version: str

    def __init__(self, local_basepath: str, config: dict = None, caches_data: dict = None, version: str = None,
                 config_filename: str = None):
        self.config_filename = config_filename
        self.version = version
        self.local_basepath = local_basepath
        self.config = config if config is not None else utils.get_config(local_basepath, self.config_filename)
        if self.config is None:
            return

        if self.config.get('force_version', False):
            if not version:
                logger.print_error('缺个版本号，使用-v或--version选项，如：hornet up --version v20181020000')
                exit(0)
            if not re.match('^[a-zA-Z0-9-]+$', version):
                logger.print_error('版本号只能由数字字母下划线横杠组成，不要使用其他符号')
                exit(0)

        self.caches_data = caches_data if caches_data is not None else caches.get(local_basepath, version)
        include: dict = self.config.get('include')
        self.include_append = include.get('append', True)
        self.include_dirs = include.get('paths', [])
        self.exclude_dirs = include.get('excludes', [])
        self.latest_version = utils.get_latest_version_code(self.local_basepath)

    def is_exclude(self, path: str) -> bool:
        if self.exclude_dirs is None or len(self.exclude_dirs) == 0:
            return False
        for it in self.exclude_dirs:
            if PurePath(path).match(it):
                return True
        return False

    def scan_file(self, server_name: str, parentpath: str, local_include_paths: List[str], remote_roots: List[str]) -> \
        List[Tuple[str, str, str, str]]:
        """
        扫描需要上传的文件
        :param server_name: 服务器名称
        :param parentpath: 扫描目录
        :param local_include_paths: 本地路径列表
        :param remote_roots: 远程上传路径列表
        :return: [(文件本地路径, 文件远程路径, 本地缓存key, 本地文件md5)]
        """
        file_list: List[Tuple[str, str, str, str]] = []
        for localpath in local_include_paths:
            realpath = parentpath + localpath
            if self.is_exclude(realpath):
                continue
            if not os.path.exists(realpath):
                continue

            if os.path.isdir(realpath):
                # 文件夹往下遍历
                paths = os.listdir(realpath)
                if paths is not None:
                    file_list += self.scan_file(server_name, realpath + '/', paths, remote_roots)
            else:
                file_md5 = file_utils.calc_file_md5(realpath)
                cache_key = '%s@%s' % (realpath, server_name)
                if self.has_cache(cache_key, file_md5):
                    continue
                local_abspath = os.path.dirname(realpath)
                if not local_abspath.endswith('/'):
                    local_abspath += '/'
                relative_path = local_abspath.replace(self.local_basepath, '').replace('\\', '/')
                if not self.include_append:
                    relative_path = self.trim_include_path(relative_path)

                for remotepath in remote_roots:
                    if not remotepath.endswith('/'):
                        remotepath += '/'
                    if self.version:
                        remotepath += self.version + '/'

                    file_list.append((realpath, remotepath + relative_path, cache_key, file_md5))
        return file_list

    def do_upload(self, transporter: SftpTransporter, file_list: List[Tuple[str, str, str, str]]):
        """
        :param SftpTransporter transporter:
        :param file_list: 文件列表[(本地路径, 远程路径)]
        """
        total_file = len(file_list)
        for idx, (realpath, remotepath, cache_key, file_md5) in enumerate(file_list):
            local_relative_path = realpath.replace(self.local_basepath, '').replace('\\', '/')
            success, remotefile = transporter.upload(realpath, remotepath)
            message = '[%s:%s] (%s/%s) %s -> %s' % (
                transporter.host, transporter.port, idx + 1, total_file, local_relative_path, remotefile)
            if success:
                # 保存上传成功的文件的MD5值
                self.caches_data[cache_key] = file_md5
                logger.print_success(message)
            else:
                logger.print_error(message)

    def trim_include_path(self, path: str) -> str:
        """
        去掉路径中包含的在配置中的路径部分
        如：hornet命令在D:/dev-workspace/hornet/ 目录下执行，配置的include.paths为['src', 'build' ...]
        参数path例如为build/static/index.js，该路径开头匹配include.paths中的build，则将该路径开头的build/部分去掉
        :param path: 相对于hornet命令执行所在的目录的相对路径
        :return: path
        """
        for p in self.include_dirs:
            if path.startswith(p):
                return re.sub(_pattern, '', path.replace(p, ''))
        return path

    def has_cache(self, cache_key: str, md5: str) -> bool:
        return self.caches_data.get(cache_key, None) == md5

    def start(self):
        if self.config is None:
            return
        server_list = utils.get_servers(self.config)
        if server_list is None or len(server_list) == 0:
            logger.print_error('没有找到服务器配置')
            return

        cmd_args = utils.parse_cmd_args()
        do_upgrade_version_if_no_file_upload = '--upgrade-version' in cmd_args
        not_only_up = '--only-up' not in cmd_args
        version_folder = None if '--not-use-version-file' in cmd_args else self.latest_version

        server: dict
        for server in server_list:
            if not server.get('enable', True):
                continue
            roots: List[str or Dict[str, str]] = server.get('roots')
            if roots is None or len(roots) == 0:
                continue
            server_name: str = server.get('name')
            if server_name is None:
                logger.print_error('请配置服务器名称`servers[*].name`')
                return

            host = server.get('host')
            if host is None:
                logger.print_error('请配置服务器地址`servers[*].host`')
                return
            port = server.get('port', 22)
            username = server.get('username', 'root')
            password = server.get('password', '')

            logger.print_info('正在扫描目录：%s' % self.local_basepath)

            roots_config_is_dict = isinstance(roots[0], dict)
            upload_roots = list(map(lambda it: it.get('source'), roots)) if roots_config_is_dict else roots
            scan_dirs = self.include_dirs if version_folder is None else list(
                map(lambda scan_path: os.path.join(scan_path, version_folder), self.include_dirs))
            file_list = self.scan_file(server_name, self.local_basepath, scan_dirs, upload_roots)
            file_count = len(file_list)
            logger.print_success('%s个文件需要上传' % file_count)

            if file_count > 0:
                logger.print_info('正在连接服务器[%s:%s]...' % (host, port))
                trans = SftpTransporter(host, port, username, password)
                trans.name = server_name
                self.do_upload(trans, file_list)
                logger.print_success('[%s:%s] 上传成功。' % (host, port))
                trans.close()
            elif not do_upgrade_version_if_no_file_upload:
                # 没有文件上传且不升级版本
                continue

            post_upload = server.get('post_upload')
            do_post = post_upload is not None and not_only_up
            do_upgrade_version = roots_config_is_dict and server.get('upgrade_version', False)

            if do_post or do_upgrade_version:
                ssh_client = ssh.SSHClient(host, port, username, password)

                # 配置了`upgrade_version`且有版本号，升级版本号
                if do_upgrade_version:
                    if version_folder is None:
                        logger.print_error('版本号为空，无法升级。')
                    else:
                        for root in roots:
                            server_source_path = utils.join_linux_path(root.get('source'), version_folder)
                            server_link_path = root.get('link')
                            logger.print_info('[%s:%s] 当前版本：%s' % (host, port, version_folder))
                            ssh_client.make_symbol_link(server_source_path, server_link_path)

                # 执行`post_upload`命令
                if do_post:
                    logger.print_info('[%s:%s] > %s' % (host, port, post_upload))
                    ssh_client.exec_command(post_upload)

                ssh_client.close()

        caches.save(self.local_basepath, self.caches_data, self.version)
